import { CesiumTerrainProvider } from '../../Source/Cesium.js';
import { Ellipsoid } from '../../Source/Cesium.js';
import { GeographicTilingScheme } from '../../Source/Cesium.js';
import { getAbsoluteUri } from '../../Source/Cesium.js';
import { HeightmapTerrainData } from '../../Source/Cesium.js';
import { IonResource } from '../../Source/Cesium.js';
import { Math as CesiumMath } from '../../Source/Cesium.js';
import { QuantizedMeshTerrainData } from '../../Source/Cesium.js';
import { Request } from '../../Source/Cesium.js';
import { RequestScheduler } from '../../Source/Cesium.js';
import { Resource } from '../../Source/Cesium.js';
import { TerrainProvider } from '../../Source/Cesium.js';
import pollToPromise from '../pollToPromise.js';
import { when } from '../../Source/Cesium.js';

describe('Core/CesiumTerrainProvider', function() {

    beforeEach(function() {
        RequestScheduler.clearForSpecs();
    });

    afterEach(function() {
        Resource._Implementations.loadWithXhr = Resource._DefaultImplementations.loadWithXhr;
    });

    function returnTileJson(path) {
        var oldLoad = Resource._Implementations.loadWithXhr;
        Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
            if (url.indexOf('layer.json') >= 0) {
                Resource._DefaultImplementations.loadWithXhr(path, responseType, method, data, headers, deferred);
            } else {
                return oldLoad(url, responseType, method, data, headers, deferred, overrideMimeType);
            }
        };
    }

    function returnHeightmapTileJson() {
        return returnTileJson('Data/CesiumTerrainTileJson/StandardHeightmap.tile.json');
    }

    function returnQuantizedMeshTileJson() {
        return returnTileJson('Data/CesiumTerrainTileJson/QuantizedMesh.tile.json');
    }

    function returnVertexNormalTileJson() {
        return returnTileJson('Data/CesiumTerrainTileJson/VertexNormals.tile.json');
    }

    function returnOctVertexNormalTileJson() {
        return returnTileJson('Data/CesiumTerrainTileJson/OctVertexNormals.tile.json');
    }

    function returnWaterMaskTileJson() {
        return returnTileJson('Data/CesiumTerrainTileJson/WaterMask.tile.json');
    }

    function returnPartialAvailabilityTileJson() {
        return returnTileJson('Data/CesiumTerrainTileJson/PartialAvailability.tile.json');
    }

    function returnParentUrlTileJson() {
        var paths = ['Data/CesiumTerrainTileJson/ParentUrl.tile.json',
                     'Data/CesiumTerrainTileJson/Parent.tile.json'];
        var i = 0;
        var oldLoad = Resource._Implementations.loadWithXhr;
        Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
            if (url.indexOf('layer.json') >= 0) {
                Resource._DefaultImplementations.loadWithXhr(paths[i++], responseType, method, data, headers, deferred);
            } else {
                return oldLoad(url, responseType, method, data, headers, deferred, overrideMimeType);
            }
        };
    }

    function returnMetadataAvailabilityTileJson() {
        return returnTileJson('Data/CesiumTerrainTileJson/MetadataAvailability.tile.json');
    }

    function waitForTile(level, x, y, requestNormals, requestWaterMask, f) {
        var terrainProvider = new CesiumTerrainProvider({
            url : 'made/up/url',
            requestVertexNormals : requestNormals,
            requestWaterMask: requestWaterMask
        });

        return pollToPromise(function() {
            return terrainProvider.ready;
        }).then(function() {
            var promise = terrainProvider.requestTileGeometry(level, x, y);

            return when(promise, f, function(error) {
                expect('requestTileGeometry').toBe('returning a tile.'); // test failure
            });
        });
    }

    function createRequest() {
        return new Request({
            throttleByServer : true
        });
    }

    it('conforms to TerrainProvider interface', function() {
        expect(CesiumTerrainProvider).toConformToInterface(TerrainProvider);
    });

    it('constructor throws if url is not provided', function() {
        expect(function() {
            return new CesiumTerrainProvider();
        }).toThrowDeveloperError();

        expect(function() {
            return new CesiumTerrainProvider({
            });
        }).toThrowDeveloperError();
    });

    it('resolves readyPromise', function() {
        var provider = new CesiumTerrainProvider({
            url : 'made/up/url'
        });

        return provider.readyPromise.then(function (result) {
            expect(result).toBe(true);
            expect(provider.ready).toBe(true);
        });
    });

    it('resolves readyPromise when url promise is used', function() {
        var provider = new CesiumTerrainProvider({
            url : when.resolve('made/up/url')
        });

        return provider.readyPromise.then(function (result) {
            expect(result).toBe(true);
            expect(provider.ready).toBe(true);
        });
    });

    it('resolves readyPromise with Resource', function() {
        var resource = new Resource({
            url : 'made/up/url'
        });

        var provider = new CesiumTerrainProvider({
            url : resource
        });

        return provider.readyPromise.then(function (result) {
            expect(result).toBe(true);
            expect(provider.ready).toBe(true);
        });
    });

    it('rejects readyPromise when url rejects', function() {
        var error = new Error();
        var provider = new CesiumTerrainProvider({
            url: when.reject(error)
        });
        return provider.readyPromise
            .then(function() {
                fail('should not resolve');
            })
            .otherwise(function(result) {
                expect(result).toBe(error);
                expect(provider.ready).toBe(false);
            });
    });

    it('uses geographic tiling scheme by default', function() {
        returnHeightmapTileJson();

        var provider = new CesiumTerrainProvider({
            url : 'made/up/url'
        });

        return pollToPromise(function() {
            return provider.ready;
        }).then(function() {
            expect(provider.tilingScheme).toBeInstanceOf(GeographicTilingScheme);
        });
    });

    it('can use a custom ellipsoid', function() {
        returnHeightmapTileJson();

        var ellipsoid = new Ellipsoid(1, 2, 3);
        var provider = new CesiumTerrainProvider({
            url : 'made/up/url',
            ellipsoid : ellipsoid
        });

        return pollToPromise(function() {
            return provider.ready;
        }).then(function() {
            expect(provider.tilingScheme.ellipsoid).toEqual(ellipsoid);
        });
    });

    it('has error event', function() {
        var provider = new CesiumTerrainProvider({
            url : 'made/up/url'
        });
        expect(provider.errorEvent).toBeDefined();
        expect(provider.errorEvent).toBe(provider.errorEvent);
    });

    it('returns reasonable geometric error for various levels', function() {
        var provider = new CesiumTerrainProvider({
            url : 'made/up/url'
        });

        expect(provider.getLevelMaximumGeometricError(0)).toBeGreaterThan(0.0);
        expect(provider.getLevelMaximumGeometricError(0)).toEqualEpsilon(provider.getLevelMaximumGeometricError(1) * 2.0, CesiumMath.EPSILON10);
        expect(provider.getLevelMaximumGeometricError(1)).toEqualEpsilon(provider.getLevelMaximumGeometricError(2) * 2.0, CesiumMath.EPSILON10);
    });

    it('logo is undefined if credit is not provided', function() {
        returnHeightmapTileJson();

        var provider = new CesiumTerrainProvider({
            url : 'made/up/url'
        });

        return pollToPromise(function() {
            return provider.ready;
        }).then(function() {
            expect(provider.credit).toBeUndefined();
        });
    });

    it('logo is defined if credit is provided', function() {
        returnHeightmapTileJson();

        var provider = new CesiumTerrainProvider({
            url : 'made/up/url',
            credit : 'thanks to our awesome made up contributors!'
        });

        return pollToPromise(function() {
            return provider.ready;
        }).then(function() {
            expect(provider.credit).toBeDefined();
        });
    });

    it('has a water mask', function() {
        returnHeightmapTileJson();

        var provider = new CesiumTerrainProvider({
            url : 'made/up/url'
        });

        return pollToPromise(function() {
            return provider.ready;
        }).then(function() {
            expect(provider.hasWaterMask).toBe(true);
        });
    });

    it('has vertex normals', function() {
        returnOctVertexNormalTileJson();

        var provider = new CesiumTerrainProvider({
            url : 'made/up/url',
            requestVertexNormals : true
        });

        return pollToPromise(function() {
            return provider.ready;
        }).then(function() {
            expect(provider.requestVertexNormals).toBe(true);
            expect(provider.hasVertexNormals).toBe(true);
        });
    });

    it('does not request vertex normals', function() {
        returnOctVertexNormalTileJson();

        var provider = new CesiumTerrainProvider({
            url : 'made/up/url',
            requestVertexNormals : false
        });

        return pollToPromise(function() {
            return provider.ready;
        }).then(function() {
            expect(provider.requestVertexNormals).toBe(false);
            expect(provider.hasVertexNormals).toBe(false);
        });
    });

    it('requests parent layer.json', function() {
        returnParentUrlTileJson();

        var provider = new CesiumTerrainProvider({
            url : 'made/up/url',
            requestVertexNormals : true,
            requestWaterMask : true
        });

        return pollToPromise(function() {
            return provider.ready;
        }).then(function() {
            expect(provider._tileCredits[0].html).toBe('This is a child tileset! This amazing data is courtesy The Amazing Data Source!');
            expect(provider.requestVertexNormals).toBe(true);
            expect(provider.requestWaterMask).toBe(true);
            expect(provider.hasVertexNormals).toBe(false); // Neither tileset has them
            expect(provider.hasWaterMask).toBe(true); // The child tileset has them
            expect(provider.availability.isTileAvailable(1, 2, 1)).toBe(true); // Both have this
            expect(provider.availability.isTileAvailable(1, 3, 1)).toBe(true); // Parent has this, but child doesn't
            expect(provider.availability.isTileAvailable(2, 0, 0)).toBe(false); // Neither has this

            var layers = provider._layers;
            expect(layers.length).toBe(2);
            expect(layers[0].hasVertexNormals).toBe(false);
            expect(layers[0].hasWaterMask).toBe(true);
            expect(layers[0].availability.isTileAvailable(1, 2, 1)).toBe(true);
            expect(layers[0].availability.isTileAvailable(1, 3, 1)).toBe(false);
            expect(layers[0].availability.isTileAvailable(2, 0, 0)).toBe(false);
            expect(layers[1].hasVertexNormals).toBe(false);
            expect(layers[1].hasWaterMask).toBe(false);
            expect(layers[1].availability.isTileAvailable(1, 2, 1)).toBe(true);
            expect(layers[1].availability.isTileAvailable(1, 3, 1)).toBe(true);
            expect(layers[1].availability.isTileAvailable(2, 0, 0)).toBe(false);
        });
    });

    it('raises an error if layer.json does not specify a format', function() {
        returnTileJson('Data/CesiumTerrainTileJson/NoFormat.tile.json');

        var provider = new CesiumTerrainProvider({
            url : 'made/up/url'
        });

        var deferred = when.defer();

        provider.errorEvent.addEventListener(function(e) {
            deferred.resolve(e);
        });

        return deferred.promise.then(function(error) {
            expect(error.message).toContain('format is not specified');
        });
    });

    it('raises an error if layer.json specifies an unknown format', function() {
        returnTileJson('Data/CesiumTerrainTileJson/InvalidFormat.tile.json');

        var provider = new CesiumTerrainProvider({
            url : 'made/up/url'
        });

        var deferred = when.defer();

        provider.errorEvent.addEventListener(function(e) {
            deferred.resolve(e);
        });

        return deferred.promise.then(function(error) {
            expect(error.message).toContain('invalid or not supported');
        });
    });

    it('raises an error if layer.json does not specify quantized-mesh 1.x format', function() {
        returnTileJson('Data/CesiumTerrainTileJson/QuantizedMesh2.0.tile.json');

        var provider = new CesiumTerrainProvider({
            url : 'made/up/url'
        });

        var deferred = when.defer();

        provider.errorEvent.addEventListener(function(e) {
            deferred.resolve(e);
        });

        return deferred.promise.then(function(error) {
            expect(error.message).toContain('invalid or not supported');
        });
    });

    it('supports quantized-mesh1.x minor versions', function() {
        returnTileJson('Data/CesiumTerrainTileJson/QuantizedMesh1.1.tile.json');

        var provider = new CesiumTerrainProvider({
            url : 'made/up/url'
        });

        var errorListener = jasmine.createSpy('error');
        provider.errorEvent.addEventListener(errorListener);

        return pollToPromise(function() {
            return provider.ready;
        }).then(function() {
            expect(errorListener).not.toHaveBeenCalled();
        });
    });

    it('raises an error if layer.json does not specify a tiles property', function() {
        returnTileJson('Data/CesiumTerrainTileJson/NoTiles.tile.json');

        var provider = new CesiumTerrainProvider({
            url : 'made/up/url'
        });

        var deferred = when.defer();

        provider.errorEvent.addEventListener(function(e) {
            deferred.resolve(e);
        });

        return deferred.promise.then(function(error) {
            expect(error.message).toContain('does not specify any tile URL templates');
        });
    });

    it('raises an error if layer.json tiles property is an empty array', function() {
        returnTileJson('Data/CesiumTerrainTileJson/EmptyTilesArray.tile.json');

        var provider = new CesiumTerrainProvider({
            url : 'made/up/url'
        });

        var deferred = when.defer();

        provider.errorEvent.addEventListener(function(e) {
            deferred.resolve(e);
        });

        return deferred.promise.then(function(error) {
            expect(error.message).toContain('does not specify any tile URL templates');
        });
    });

    it('uses attribution specified in layer.json', function() {
        returnTileJson('Data/CesiumTerrainTileJson/WithAttribution.tile.json');

        var provider = new CesiumTerrainProvider({
            url : 'made/up/url'
        });

        return pollToPromise(function() {
            return provider.ready;
        }).then(function() {
            expect(provider._tileCredits[0].html).toBe('This amazing data is courtesy The Amazing Data Source!');
        });
    });

    it('do not add blank attribution if layer.json does not have one', function() {
        returnTileJson('Data/CesiumTerrainTileJson/WaterMask.tile.json');

        var provider = new CesiumTerrainProvider({
            url : 'made/up/url'
        });

        return pollToPromise(function() {
            return provider.ready;
        }).then(function() {
            expect(provider._tileCredit).toBeUndefined();
        });
    });

    it('The undefined availability tile is returned at level 0', function() {
        var layer = {
            availabilityLevels: 10
        };

        expect(CesiumTerrainProvider._getAvailabilityTile(layer, 0, 0, 0)).toBeUndefined();
        expect(CesiumTerrainProvider._getAvailabilityTile(layer, 1, 0, 0)).toBeUndefined();
    });

    it('The correct availability tile is computed in first level', function() {
        var layer = {
            availabilityLevels: 10
        };

        expect(CesiumTerrainProvider._getAvailabilityTile(layer, 1, 1, 1)).toEqual({
            level: 0,
            x: 0,
            y: 0
        });
        expect(CesiumTerrainProvider._getAvailabilityTile(layer, 4, 2, 2)).toEqual({
            level: 0,
            x: 1,
            y: 0
        });

        expect(CesiumTerrainProvider._getAvailabilityTile(layer, 80, 50, 10)).toEqual({
            level: 0,
            x: 0,
            y: 0
        });
    });

    it('The correct availability tile is computed in second level', function() {
        var layer = {
            availabilityLevels: 10
        };

        var expected = {
            level: 10,
            x: 80,
            y: 50
        };

        var xs = [expected.x, expected.x];
        var ys = [expected.y, expected.y];

        // Compute level 20 tiles by always taking SW or NE child
        for (var i = 0; i < 10; ++i) {
            xs[0] *= 2;
            ys[0] *= 2;
            xs[1] = xs[1] * 2 + 1;
            ys[1] = ys[1] * 2 + 1;
        }

        expect(CesiumTerrainProvider._getAvailabilityTile(layer, xs[0], ys[0], 20)).toEqual(expected);
        expect(CesiumTerrainProvider._getAvailabilityTile(layer, xs[1], ys[1], 20)).toEqual(expected);
    });

    describe('requestTileGeometry', function() {

        it('uses multiple urls specified in layer.json', function() {
            returnTileJson('Data/CesiumTerrainTileJson/MultipleUrls.tile.json');

            var provider = new CesiumTerrainProvider({
                url : 'made/up/url'
            });

            return pollToPromise(function() {
                return provider.ready;
            }).then(function() {
                spyOn(Resource._Implementations, 'loadWithXhr');
                provider.requestTileGeometry(0, 0, 0);
                expect(Resource._Implementations.loadWithXhr.calls.mostRecent().args[0]).toContain('foo0.com');
                provider.requestTileGeometry(1, 0, 0);
                expect(Resource._Implementations.loadWithXhr.calls.mostRecent().args[0]).toContain('foo1.com');
                provider.requestTileGeometry(1, -1, 0);
                expect(Resource._Implementations.loadWithXhr.calls.mostRecent().args[0]).toContain('foo2.com');
                provider.requestTileGeometry(1, 0, 1);
                expect(Resource._Implementations.loadWithXhr.calls.mostRecent().args[0]).toContain('foo3.com');
            });
        });

        it('supports scheme-less template URLs in layer.json resolved with absolute URL', function() {
            returnTileJson('Data/CesiumTerrainTileJson/MultipleUrls.tile.json');

            var url = getAbsoluteUri('Data/CesiumTerrainTileJson');

            var provider = new CesiumTerrainProvider({
                url : url
            });

            return pollToPromise(function() {
                return provider.ready;
            }).then(function() {
                spyOn(Resource._Implementations, 'loadWithXhr');
                provider.requestTileGeometry(0, 0, 0);
                expect(Resource._Implementations.loadWithXhr.calls.mostRecent().args[0]).toContain('foo0.com');
                provider.requestTileGeometry(1, 0, 0);
                expect(Resource._Implementations.loadWithXhr.calls.mostRecent().args[0]).toContain('foo1.com');
                provider.requestTileGeometry(1, -1, 0);
                expect(Resource._Implementations.loadWithXhr.calls.mostRecent().args[0]).toContain('foo2.com');
                provider.requestTileGeometry(1, 0, 1);
                expect(Resource._Implementations.loadWithXhr.calls.mostRecent().args[0]).toContain('foo3.com');
            });
        });

        it('provides HeightmapTerrainData', function() {
            Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                // Just return any old file, as long as its big enough
                Resource._DefaultImplementations.loadWithXhr('Data/EarthOrientationParameters/IcrfToFixedStkComponentsRotationData.json', responseType, method, data, headers, deferred);
            };

            returnHeightmapTileJson();

            return waitForTile(0, 0, 0, false, false, function(loadedData) {
                expect(loadedData).toBeInstanceOf(HeightmapTerrainData);
            });
        });

        it('provides QuantizedMeshTerrainData', function() {
            Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                Resource._DefaultImplementations.loadWithXhr('Data/CesiumTerrainTileJson/tile.terrain', responseType, method, data, headers, deferred);
            };

            returnQuantizedMeshTileJson();

            return waitForTile(0, 0, 0, false, false, function(loadedData) {
                expect(loadedData).toBeInstanceOf(QuantizedMeshTerrainData);
            });
        });

        it('provides QuantizedMeshTerrainData with 32bit indices', function() {
            Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                Resource._DefaultImplementations.loadWithXhr('Data/CesiumTerrainTileJson/tile.32bitIndices.terrain', responseType, method, data, headers, deferred);
            };

            returnQuantizedMeshTileJson();

            return waitForTile(0, 0, 0, false, false, function(loadedData) {
                expect(loadedData).toBeInstanceOf(QuantizedMeshTerrainData);
                expect(loadedData._indices.BYTES_PER_ELEMENT).toBe(4);
            });
        });

        it('provides QuantizedMeshTerrainData with VertexNormals', function() {
            Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                Resource._DefaultImplementations.loadWithXhr('Data/CesiumTerrainTileJson/tile.vertexnormals.terrain', responseType, method, data, headers, deferred);
            };

            returnVertexNormalTileJson();

            return waitForTile(0, 0, 0, true, false, function(loadedData) {
                expect(loadedData).toBeInstanceOf(QuantizedMeshTerrainData);
                expect(loadedData._encodedNormals).toBeDefined();
            });
        });

        it('provides QuantizedMeshTerrainData with WaterMask', function() {
            Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                Resource._DefaultImplementations.loadWithXhr('Data/CesiumTerrainTileJson/tile.watermask.terrain', responseType, method, data, headers, deferred);
            };

            returnWaterMaskTileJson();

            return waitForTile(0, 0, 0, false, true, function(loadedData) {
                expect(loadedData).toBeInstanceOf(QuantizedMeshTerrainData);
                expect(loadedData._waterMask).toBeDefined();
            });
        });

        it('provides QuantizedMeshTerrainData with VertexNormals and WaterMask', function() {
            Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                Resource._DefaultImplementations.loadWithXhr('Data/CesiumTerrainTileJson/tile.octvertexnormals.watermask.terrain', responseType, method, data, headers, deferred);
            };

            returnWaterMaskTileJson();

            return waitForTile(0, 0, 0, true, true, function(loadedData) {
                expect(loadedData).toBeInstanceOf(QuantizedMeshTerrainData);
                expect(loadedData._encodedNormals).toBeDefined();
                expect(loadedData._waterMask).toBeDefined();
            });
        });

        it('provides QuantizedMeshTerrainData with OctVertexNormals', function() {
            Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                Resource._DefaultImplementations.loadWithXhr('Data/CesiumTerrainTileJson/tile.octvertexnormals.terrain', responseType, method, data, headers, deferred);
            };

            returnOctVertexNormalTileJson();

            return waitForTile(0, 0, 0, true, false, function(loadedData) {
                expect(loadedData).toBeInstanceOf(QuantizedMeshTerrainData);
                expect(loadedData._encodedNormals).toBeDefined();
            });
        });

        it('provides QuantizedMeshTerrainData with VertexNormals and unknown extensions', function() {
            Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                Resource._DefaultImplementations.loadWithXhr('Data/CesiumTerrainTileJson/tile.vertexnormals.unknownext.terrain', responseType, method, data, headers, deferred);
            };

            returnVertexNormalTileJson();

            return waitForTile(0, 0, 0, true, false, function(loadedData) {
                expect(loadedData).toBeInstanceOf(QuantizedMeshTerrainData);
                expect(loadedData._encodedNormals).toBeDefined();
            });
        });

        it('provides QuantizedMeshTerrainData with OctVertexNormals and unknown extensions', function() {
            Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                Resource._DefaultImplementations.loadWithXhr('Data/CesiumTerrainTileJson/tile.octvertexnormals.unknownext.terrain', responseType, method, data, headers, deferred);
            };

            returnOctVertexNormalTileJson();

            return waitForTile(0, 0, 0, true, false, function(loadedData) {
                expect(loadedData).toBeInstanceOf(QuantizedMeshTerrainData);
                expect(loadedData._encodedNormals).toBeDefined();
            });
        });

        it('provides QuantizedMeshTerrainData with unknown extension', function() {
            Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                Resource._DefaultImplementations.loadWithXhr('Data/CesiumTerrainTileJson/tile.unknownext.terrain', responseType, method, data, headers, deferred);
            };

            returnOctVertexNormalTileJson();

            return waitForTile(0, 0, 0, false, false, function(loadedData) {
                expect(loadedData).toBeInstanceOf(QuantizedMeshTerrainData);
            });
        });

        it('provides QuantizedMeshTerrainData with Metadata availability', function() {
            Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                Resource._DefaultImplementations.loadWithXhr('Data/CesiumTerrainTileJson/tile.metadataavailability.terrain', responseType, method, data, headers, deferred);
            };

            returnMetadataAvailabilityTileJson();

            var terrainProvider = new CesiumTerrainProvider({
                url : 'made/up/url'
            });

            return pollToPromise(function() {
                return terrainProvider.ready;
            }).then(function() {
                expect(terrainProvider.hasMetadata).toBe(true);
                expect(terrainProvider._layers[0].availabilityLevels).toBe(10);
                expect(terrainProvider.availability.isTileAvailable(0,0,0)).toBe(true);
                expect(terrainProvider.availability.isTileAvailable(0,1,0)).toBe(true);
                expect(terrainProvider.availability.isTileAvailable(1,0,0)).toBe(false);

                return terrainProvider.requestTileGeometry(0, 0, 0);
            }).then(function(loadedData) {
                expect(loadedData).toBeInstanceOf(QuantizedMeshTerrainData);
                expect(terrainProvider.availability.isTileAvailable(1,0,0)).toBe(true);
            });
        });

        it('returns undefined if too many requests are already in progress', function() {
            var baseUrl = 'made/up/url';

            var deferreds = [];

            Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                // Do nothing, so requests never complete
                deferreds.push(deferred);
            };

            returnHeightmapTileJson();

            var terrainProvider = new CesiumTerrainProvider({
                url : baseUrl
            });

            return pollToPromise(function() {
                return terrainProvider.ready;
            }).then(function() {
                var promise;
                var i;
                for (i = 0; i < RequestScheduler.maximumRequestsPerServer; ++i) {
                    promise = terrainProvider.requestTileGeometry(0, 0, 0, createRequest());
                }
                RequestScheduler.update();
                expect(promise).toBeDefined();

                promise = terrainProvider.requestTileGeometry(0, 0, 0, createRequest());
                expect(promise).toBeUndefined();

                for (i = 0; i < deferreds.length; ++i) {
                    deferreds[i].resolve();
                }
            });
        });

        it('supports getTileDataAvailable()', function() {
            var baseUrl = 'made/up/url';

            Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                Resource._DefaultImplementations.loadWithXhr('Data/CesiumTerrainTileJson/tile.terrain', responseType, method, data, headers, deferred);
            };

            returnQuantizedMeshTileJson();

            var terrainProvider = new CesiumTerrainProvider({
                url : baseUrl
            });

            return pollToPromise(function() {
                return terrainProvider.ready;
            }).then(function() {
                expect(terrainProvider.getTileDataAvailable(0, 0, 0)).toBe(true);
                expect(terrainProvider.getTileDataAvailable(0, 0, 2)).toBe(false);
            });
        });

        it('getTileDataAvailable() converts xyz to tms', function() {
            var baseUrl = 'made/up/url';

            returnPartialAvailabilityTileJson();

            var terrainProvider = new CesiumTerrainProvider({
                url : baseUrl
            });

            return pollToPromise(function() {
                return terrainProvider.ready;
            }).then(function() {
                expect(terrainProvider.getTileDataAvailable(1, 3, 2)).toBe(true);
                expect(terrainProvider.getTileDataAvailable(1, 0, 2)).toBe(false);
            });
        });

        it('getTileDataAvailable() with Metadata availability', function() {
            Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                Resource._DefaultImplementations.loadWithXhr('Data/CesiumTerrainTileJson/tile.metadataavailability.terrain', responseType, method, data, headers, deferred);
            };

            returnMetadataAvailabilityTileJson();

            var terrainProvider = new CesiumTerrainProvider({
                url : 'made/up/url'
            });

            return pollToPromise(function() {
                return terrainProvider.ready;
            }).then(function() {
                expect(terrainProvider.getTileDataAvailable(0,0,0)).toBe(true);
                expect(terrainProvider.getTileDataAvailable(0,0,1)).toBeUndefined();

                return terrainProvider.requestTileGeometry(0, 0, 0);
            }).then(function() {
                expect(terrainProvider.getTileDataAvailable(0,0,1)).toBe(true);
            });
        });

        it('supports a query string in the base URL', function() {
            Resource._Implementations.loadWithXhr = function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                // Just return any old file, as long as its big enough
                Resource._DefaultImplementations.loadWithXhr('Data/EarthOrientationParameters/IcrfToFixedStkComponentsRotationData.json', responseType, method, data, headers, deferred);
            };

            returnHeightmapTileJson();

            return waitForTile(0, 0, 0, false, false, function(loadedData) {
                expect(loadedData).toBeInstanceOf(HeightmapTerrainData);
            });
        });

        it('Uses query parameter extensions for ion resource', function() {
            var terrainProvider = new CesiumTerrainProvider({
                url: IonResource.fromAssetId(1),
                requestVertexNormals: true,
                requestWaterMask: true
            });

            return pollToPromise(function() {
                return terrainProvider.ready;
            }).then(function() {
                var getDerivedResource = spyOn(IonResource.prototype, 'getDerivedResource').and.callThrough();
                terrainProvider.requestTileGeometry(0, 0, 0);
                var options = getDerivedResource.calls.argsFor(0)[0];
                expect(options.queryParameters.extensions).toEqual('octvertexnormals-watermask-metadata');
            });
        });
    });
});
